Skip to content

fix(observability): report the miner's real input/output token split to PostHog - #10199

Merged
loopover-orb[bot] merged 1 commit into
mainfrom
feat/miner-engine-ai-observability
Jul 31, 2026
Merged

fix(observability): report the miner's real input/output token split to PostHog#10199
loopover-orb[bot] merged 1 commit into
mainfrom
feat/miner-engine-ai-observability

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

The miner reported 0 input tokens and 0 output tokens on every $ai_generation, so PostHog's own LLM cost views were blind to AMS spend entirely. The real figure was emitted, but under a non-standard tokens_used property those views do not read.

captureMinerPostHogAiGeneration hardcoded the two properties, and its doc comment justified that as honest — "there is no input/output split available at this layer". That was true of that layer, but the layer below threw the split away: both engine drivers read the two sides and returned only their sum.

  • agent-sdk-driver.ts read usage.input_tokens / usage.output_tokens, then returned (inputTokens ?? 0) + (outputTokens ?? 0).
  • cli-subprocess-driver.ts did the same with usage.inputTokens / usage.outputTokens.

So the fix is not to relabel anything — it is to stop discarding data that was already in hand. CodingAgentDriverResult now carries inputTokens/outputTokens alongside the existing blended tokensUsed, both drivers populate them from values they already read, and the miner reports them.

The never-fabricate convention costUsd and tokensUsed already follow is preserved throughout:

  • A CLI that reports only total_tokens genuinely has no split; it leaves both sides absent and the blended figure keeps riding in tokens_used. Deriving a split from a total would be an invention.
  • A side that is missing, or out-of-contract (negative / NaN / Infinity), stays absent rather than becoming a 0 — once aggregated, a fabricated 0 is indistinguishable from a real one.
  • 0 therefore survives as the miner-side fallback only where it means "no split known".

Each driver's token fields are produced by a single expression and spread into every return site, so a driver cannot report a split that disagrees with its own blended total.

Closes #10198

Scope

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked a currently open issue this PR resolves (e.g. Closes #123) — a linked open issue is required for every contributor PR.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally; codecov/patch requires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

If any required check was skipped, explain why:

  • npm run build:miner and the @loopover/engine workspace suite (942 tests) were both run and are green; the engine package was rebuilt before every test run so the dist/-importing engine tests exercised the new code rather than a stale build.
  • Coverage was measured scoped to the five changed files rather than via a full test:coverage run: 100% of the changed lines AND branches in all five, verified line-by-line against the lcov report rather than read off a summary percentage. Engine behaviour is covered from packages/loopover-engine/test/** as well as the root suite, so the engine upload credits it independently.
  • The unchecked commands cover untouched surfaces (no workflow, worker binding, OpenAPI schema, or UI file changes here) and are left to CI.

Tests added

Two existing tests asserted the hardcoded zeros and the "no fabricated split" rationale; both were updated to the corrected expectation rather than deleted, since the rationale itself is what changed.

  • Engine (packages/loopover-engine/test/agent-sdk-driver.test.ts): the split alongside the blended total; the split riding the failure results too, exactly like tokensUsed/costUsd (the session was billed either way); and a side left absent rather than zeroed when it is missing, out-of-contract, or when usage is absent entirely.
  • CLI driver: the split reported alongside the total; an explicit total_tokens kept as the blended figure without inventing a split from it; total_tokens plus both sides all reported together (the CLI's own total still wins over the sum, unchanged); and a single reported side left absent on the other.
  • Miner: the real split reaching $ai_input_tokens/$ai_output_tokens, and the 0-fallback for a driver that only knows a blended total, including a partially-reported case.
  • withCodingAgentAiGenerationCapture: forwards cost, blended tokens and the split verbatim, and leaves the split at 0 for a driver that reports only a total.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests.
  • API/OpenAPI/MCP behavior is updated and tested where needed.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks.
  • Visible UI changes include a UI Evidence section below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs.

Token counts are metadata, not content: no prompt, diff, or transcript text is added to any event by this change, and the existing assertions that $ai_input/$ai_output_choices are never present still hold.

UI Evidence

Not applicable — no visible UI, frontend, docs, or extension change.

Notes

The blended tokens_used property is deliberately kept rather than replaced. It is the only figure available for a provider that reports no split, and dropping it would lose data for exactly the callers that have the least of it.

Two AMS surfaces remain uninstrumented and are out of scope here — runChatGrounding (packages/loopover-engine/src/miner/chat-grounding.ts) and the runCodingAgentAttempt path, which calls createCodingAgentDriver directly and so bypasses constructProductionCodingAgentDriver's capture wrapper entirely. Both warrant their own issue and change.

…to PostHog

Both engine coding-agent drivers read input and output tokens separately and then
returned only their sum, so CodingAgentDriverResult carried a single blended
tokensUsed. The miner's $ai_generation capture had nothing real to put in
$ai_input_tokens/$ai_output_tokens and hardcoded them to 0, emitting the true
figure under a non-standard tokens_used property that PostHog's own cost views do
not read -- so miner spend registered there as zero tokens across the board.

Carry inputTokens/outputTokens on the driver result, populated by both drivers
from values they already had, and report them. The split is still never
fabricated: a CLI that reports only total_tokens leaves it absent and the blended
figure keeps riding in tokens_used, and a side that is missing or out-of-contract
(negative/NaN/Infinity) stays absent rather than becoming a 0 that is
indistinguishable from a real 0 once aggregated.

Closes #10198
@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-31 12:37:01 UTC

9 files · 1 AI reviewer · no blockers · readiness 98/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR correctly traces the root cause to the two engine drivers summing input/output tokens together before the miner-side PostHog capture ever sees the split, then plumbs the real inputTokens/outputTokens fields through CodingAgentDriverResult, both drivers, and captureMinerPostHogAiGeneration. The never-fabricate convention (absent rather than zeroed for missing/out-of-contract sides) is applied consistently and matches the existing costUsd/tokensUsed pattern already in the codebase. Tests cover the split-present, split-absent, and partial-side cases across both drivers and the PostHog capture layer, and CI passed on this commit.

Nits — 5 non-blocking
  • packages/loopover-miner/lib/posthog.ts:150-151 — Number.isFinite(event.inputTokens) doesn't reject negative values the way finiteNonNegativeNumber does upstream; a negative inputTokens would pass through as-is instead of falling back to 0, though in practice the drivers already filter negatives before this layer.
  • The external brief's '10198' magic-number complaint refers to a comment reference (ai(observability): the miner reports 0 input/output tokens because both drivers sum the split away #10198), not a real numeric literal in logic, so it's not actionable.
  • coding-agent-driver.ts's CodingAgentTokenUsage type is a nice touch for guaranteeing the spread sites can't diverge from the blended total, worth calling out as a good pattern for other drivers to follow.
  • Consider validating event.inputTokens/outputTokens are non-negative in posthog.ts's captureMinerPostHogAiGeneration, mirroring finiteNonNegativeNumber's stricter check used elsewhere, for defense-in-depth against a future driver that doesn't pre-validate.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #10198
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 9 registered-repo PR(s), 8 merged, 310 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 9 PR(s), 310 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff carries the real input/output split through CodingAgentDriverResult in both drivers, forwards it via withCodingAgentAiGenerationCapture, and populates $ai_input_tokens/$ai_output_tokens from the real values, falling back to absent/0 only when a provider genuinely reports no split, matching all listed deliverables.

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 9 PR(s), 310 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored self-assigned this Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.22%. Comparing base (3884485) to head (61bfa1b).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #10199   +/-   ##
=======================================
  Coverage   92.21%   92.22%           
=======================================
  Files         934      934           
  Lines      114178   114210   +32     
  Branches    27593    27599    +6     
=======================================
+ Hits       105294   105326   +32     
  Misses       7582     7582           
  Partials     1302     1302           
Flag Coverage Δ
backend 95.68% <100.00%> (+<0.01%) ⬆️
engine 73.82% <90.90%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ages/loopover-engine/src/miner/agent-sdk-driver.ts 95.58% <100.00%> (+0.18%) ⬆️
...loopover-engine/src/miner/cli-subprocess-driver.ts 75.12% <100.00%> (+0.49%) ⬆️
...s/loopover-engine/src/miner/coding-agent-driver.ts 100.00% <100.00%> (ø)
...es/loopover-miner/lib/coding-agent-construction.ts 100.00% <ø> (ø)
packages/loopover-miner/lib/posthog.ts 100.00% <100.00%> (ø)

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit c7645de into main Jul 31, 2026
8 checks passed
@loopover-orb
loopover-orb Bot deleted the feat/miner-engine-ai-observability branch July 31, 2026 12:37
loopover-orb Bot pushed a commit that referenced this pull request Jul 31, 2026
…nt (#10212)

SECRET_KEY matches /token/i, so scrubRecord -- wired as posthog-node's
before_send -- rewrote PostHog's own $ai_input_tokens and $ai_output_tokens to
the "[redacted]" STRING, which PostHog then coerced to null on its
numerically-typed properties.

The result: not one AI call in the project has ever carried a token count.
posthog.ai_events.input_tokens/output_tokens/total_tokens are NULL for every
model over the retention window, including claude-sonnet-5 at 2,321 calls and
$498.43 of real spend. $ai_total_cost_usd came through untouched because it has
no secret-shaped word in it. PostHog derives $ai_input_cost_usd/$ai_output_cost_usd
from tokens, so those could not be computed either -- and the miner-side split
landed in #10199 would have been scrubbed the same way.

A secret-shaped key holding a NUMBER is a counter, not a credential: every secret
this module exists to catch is a string, and there is no numeric form of one to
leak. Skip redaction for numbers only; a string, object, array or boolean under
the same key is still redacted exactly as before.

Deliberately general rather than an allowlist of the two $ai_* keys -- an
allowlist goes stale the moment PostHog adds $ai_cache_read_input_tokens, and it
would fail the same silent way.

Closes #10211
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ai(observability): the miner reports 0 input/output tokens because both drivers sum the split away

1 participant